 /*******************************************************************************
  * Copyright (c) 2000, 2007 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  * James D Miles (IBM Corp.) - bug 176250, Configurator needs to handle more platform urls
  *******************************************************************************/
 package org.eclipse.update.internal.configurator;

 import java.io.BufferedWriter ;
 import java.io.File ;
 import java.io.FileFilter ;
 import java.io.FileInputStream ;
 import java.io.FileOutputStream ;
 import java.io.IOException ;
 import java.io.OutputStream ;
 import java.io.OutputStreamWriter ;
 import java.io.SyncFailedException ;
 import java.io.UnsupportedEncodingException ;
 import java.lang.reflect.InvocationTargetException ;
 import java.net.MalformedURLException ;
 import java.net.URL ;
 import java.net.URLConnection ;
 import java.util.ArrayList ;
 import java.util.Date ;
 import java.util.HashMap ;
 import java.util.HashSet ;
 import java.util.Properties ;
 import java.util.Set ;

 import javax.xml.parsers.DocumentBuilder ;
 import javax.xml.parsers.DocumentBuilderFactory ;

 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.FileLocator;
 import org.eclipse.osgi.service.datalocation.Location;
 import org.eclipse.osgi.util.NLS;
 import org.eclipse.update.configurator.IPlatformConfiguration;
 import org.w3c.dom.Document ;
 import org.w3c.dom.Element ;

 /**
  * This class is responsible for providing the features and plugins (bundles) to
  * the runtime. Configuration data is stored in the configuration/org.eclipse.update/platform.xml file.
  * When eclipse starts, it tries to load the config info from platform.xml.
  * If the file does not exist, then it also tries to read it from a temp or backup file.
  * If this does not succeed, a platform.xml is created by inspecting the eclipse
  * installation directory (its features and plugin folders).
  * If platform.xml already exists, a check is made to see when it was last modified
  * and whether there are any file system changes that are newer (users may manually unzip
  * features and plugins). In this case, the newly added features and plugins are picked up.
  * A check for existence of features and plugins is also performed, to detect deletions.
  */
 public class PlatformConfiguration implements IPlatformConfiguration, IConfigurationConstants {

     private static PlatformConfiguration currentPlatformConfiguration = null;
     //private static final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
 // private static final TransformerFactory transformerFactory = TransformerFactory.newInstance();
 private static final String XML_ENCODING = "UTF-8"; //$NON-NLS-1$

     private Configuration config;
     private URL configLocation;
     private HashMap externalLinkSites; // used to restore prior link site state
 private long changeStamp;
     private long featuresChangeStamp;
     private boolean featuresChangeStampIsValid;
     private long pluginsChangeStamp;
     private boolean pluginsChangeStampIsValid;
     //PAL nio optional
 //private FileLock lock;
 private Locker lock = null;
     private static int defaultPolicy = DEFAULT_POLICY_TYPE;
     private static boolean checkNio = false;
     private static boolean useNio;

     private static final String ECLIPSE = "eclipse"; //$NON-NLS-1$
 private static final String CONFIG_HISTORY = "history"; //$NON-NLS-1$
 private static final String PLATFORM_XML = "platform.xml"; //$NON-NLS-1$
 private static final String CONFIG_NAME = ConfigurationActivator.NAME_SPACE + "/" + PLATFORM_XML; //$NON-NLS-1$
 private static final String CONFIG_INI = "config.ini"; //NON-NLS-1$ //$NON-NLS-1$
 private static final String CONFIG_FILE_LOCK_SUFFIX = ".lock"; //$NON-NLS-1$
 private static final String CONFIG_FILE_TEMP_SUFFIX = ".tmp"; //$NON-NLS-1$
 private static final String LINKS = "links"; //$NON-NLS-1$
 private static final String [] BOOTSTRAP_PLUGINS = {};

     private static final String DEFAULT_FEATURE_APPLICATION = "org.eclipse.ui.ide.workbench"; //$NON-NLS-1$

     private static final String LINK_PATH = "path"; //$NON-NLS-1$
 private static final String LINK_READ = "r"; //$NON-NLS-1$
 private static final String LINK_READ_WRITE = "rw"; //$NON-NLS-1$
 private static URL installURL;
     
     private PlatformConfiguration(Location platformConfigLocation) throws CoreException, IOException {

         this.externalLinkSites = new HashMap ();
         this.config = null;
         
         // initialize configuration
 initializeCurrent(platformConfigLocation);
         if(config != null)
             setDefaultPolicy();

         // Detect external links. These are "soft link" to additional sites. The link
 // files are usually provided by external installation programs. They are located
 // relative to this configuration URL.
 // Note: don't do it for self hosting
 if (!isTransient())
             configureExternalLinks();

         // Validate sites in the configuration. Causes any sites that do not exist to
 // be removed from the configuration
 validateSites();

         // compute differences between configuration and actual content of the sites
 // (base sites and link sites)
 // Note: when the config is transient (generated by PDE, etc.) we don't reconcile
 if (isTransient())
             return;
         
         // for 'osgi.clean' or osgi.checkConfiguration', force a refresh
 boolean osgiClean = "true".equals(ConfigurationActivator.getBundleContext().getProperty("osgi.clean")); //$NON-NLS-1$ //$NON-NLS-2$
 boolean osgiCheckConfiguration = "true".equals(ConfigurationActivator.getBundleContext().getProperty("osgi.checkConfiguration")); //$NON-NLS-1$ //$NON-NLS-2$

         if (osgiClean || osgiCheckConfiguration) {
             // We have to call refresh() for features to be rescanned correctly
 refresh();
             reconcile();
         }
         else {
             changeStamp = computeChangeStamp();
             if (changeStamp > config.getDate().getTime())
                 reconcile();
         }
     }

     PlatformConfiguration(URL url) throws Exception {
         this.externalLinkSites = new HashMap ();
         initialize(url);
     }

     private void setDefaultPolicy() {
         // Assumption: If the configuration that we initialize with
 // has a MANAGED_ONLY policy, then all sites should have default policy
 // of MANAGED_ONLY.
 ISiteEntry[] sentries = getConfiguredSites();
         if(sentries != null && sentries.length >0){
             int policyType = sentries[0].getSitePolicy().getType();
             if(policyType == ISitePolicy.MANAGED_ONLY){
                 defaultPolicy = policyType;
             }
         }
     }

     public static int getDefaultPolicy(){
         return defaultPolicy;
     }

     /*
      * @see IPlatformConfiguration#createSiteEntry(URL, ISitePolicy)
      */
     public ISiteEntry createSiteEntry(URL url, ISitePolicy policy) {
         return new SiteEntry(url, policy);
     }

     /*
      * @see IPlatformConfiguration#createSitePolicy(int, String[])
      */
     public ISitePolicy createSitePolicy(int type, String [] list) {
         return new SitePolicy(type, list);
     }

     /*
      * @see IPlatformConfiguration#createFeatureEntry(String, String, String, boolean, String, URL)
      */
     public IFeatureEntry createFeatureEntry(String id, String version, String pluginVersion, boolean primary, String application, URL [] root) {
         return new FeatureEntry(id, version, pluginVersion, primary, application, root);
     }

     /*
      * @see IPlatformConfiguration#createFeatureEntry(String, String, String,
      * String, boolean, String, URL)
      */
     public IFeatureEntry createFeatureEntry(String id, String version, String pluginIdentifier, String pluginVersion, boolean primary, String application, URL [] root) {
         return new FeatureEntry(id, version, pluginIdentifier, pluginVersion, primary, application, root);
     }

     /*
      * @see IPlatformConfiguration#configureSite(ISiteEntry)
      */
     public void configureSite(ISiteEntry entry) {
         configureSite(entry, false);
     }

     /*
      * @see IPlatformConfiguration#configureSite(ISiteEntry, boolean)
      */
     public synchronized void configureSite(ISiteEntry entry, boolean replace) {

         if (entry == null)
             return;
     
         URL url = entry.getURL();
         if (url == null)
             return;

         String key = url.toExternalForm();
         if (config.getSiteEntry(key) != null && !replace)
             return;
     
         if (entry instanceof SiteEntry)
             config.addSiteEntry(key, (SiteEntry)entry);
     }

     /*
      * @see IPlatformConfiguration#unconfigureSite(ISiteEntry)
      */
     public synchronized void unconfigureSite(ISiteEntry entry) {
         if (entry == null)
             return;

         URL url = entry.getURL();
         if (url == null)
             return;
         
         String key = url.toExternalForm();
         if (entry instanceof SiteEntry)
             config.removeSiteEntry(key);
     }

     /*
      * @see IPlatformConfiguration#getConfiguredSites()
      */
     public ISiteEntry[] getConfiguredSites() {
         if (config == null)
             return new ISiteEntry[0];
         
         SiteEntry[] sites = config.getSites();
         ArrayList enabledSites = new ArrayList (sites.length);
         for (int i=0; i<sites.length; i++) {
             if (sites[i].isEnabled())
                 enabledSites.add(sites[i]);
         }
         return (ISiteEntry[])enabledSites.toArray(new ISiteEntry[enabledSites.size()]);
     }

     /*
      * @see IPlatformConfiguration#findConfiguredSite(URL)
      */
     public ISiteEntry findConfiguredSite(URL url) {
         return findConfiguredSite(url, true);
     }
     
     /**
      *
      * @param url site url
      * @param checkPlatformURL if true, check for url format that is platform:/...
      * @return
      */
     public SiteEntry findConfiguredSite(URL url, boolean checkPlatformURL) {
         if (url == null)
             return null;
         String key = url.toExternalForm();

         SiteEntry result = config.getSiteEntry(key);
         if (result == null) { // retry with decoded URL string
 try {
                 //PAL foundation
 //key = URLDecoder.decode(key, "UTF-8"); //$NON-NLS-1$
 key = UpdateURLDecoder.decode(key, "UTF-8"); //$NON-NLS-1$
 } catch (UnsupportedEncodingException e) {
                 // ignore
 }
             result = config.getSiteEntry(key);
         }
             
         if (result == null && checkPlatformURL) {
             try {
                 result = findConfiguredSite(config.asPlatformURL(url), false);
             } catch (Exception e) {
                 //ignore
 }
         }
         return result;
     }

     /*
      * @see IPlatformConfiguration#configureFeatureEntry(IFeatureEntry)
      */
     public synchronized void configureFeatureEntry(IFeatureEntry entry) {
         if (entry == null)
             return;

         String key = entry.getFeatureIdentifier();
         if (key == null)
             return;

         // we should check each site and find where the feature is
 // located and then configure it
 if (config == null)
             config = new Configuration();

         SiteEntry[] sites = config.getSites();
         for (int i=0; i<sites.length; i++) {
             // find out what site contains the feature and configure it
 try {
                 URL url = new URL (sites[i].getURL(), FEATURES + "/" + entry.getFeatureIdentifier()+ "_" + entry.getFeatureVersion() + "/"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
 if (new File (url.getFile()).exists())
                     sites[i].addFeatureEntry(entry);
                 else {
                     url = new URL (sites[i].getURL(), FEATURES + "/" + entry.getFeatureIdentifier() + "/"); //$NON-NLS-1$ //$NON-NLS-2$
 if (new File (url.getFile()).exists())
                         sites[i].addFeatureEntry(entry);
                 }
             } catch (MalformedURLException e) {
             }
         }
     }

     /*
      * @see IPlatformConfiguration#unconfigureFeatureEntry(IFeatureEntry)
      */
     public synchronized void unconfigureFeatureEntry(IFeatureEntry entry) {
         if (entry == null)
             return;

         String key = entry.getFeatureIdentifier();
         if (key == null)
             return;

         config.unconfigureFeatureEntry(entry);
     }

     /*
      * @see IPlatformConfiguration#getConfiguredFeatureEntries()
      */
     public IFeatureEntry[] getConfiguredFeatureEntries() {
         ArrayList configFeatures = new ArrayList ();
         SiteEntry[] sites = config.getSites();
         for (int i=0; i<sites.length; i++) {
             FeatureEntry[] features = sites[i].getFeatureEntries();
             for (int j=0; j<features.length; j++)
                 configFeatures.add(features[j]);
         }
         return (IFeatureEntry[])configFeatures.toArray(new FeatureEntry[configFeatures.size()]);
     }

     /*
      * @see IPlatformConfiguration#findConfiguredFeatureEntry(String)
      */
     public IFeatureEntry findConfiguredFeatureEntry(String id) {
         if (id == null)
             return null;

         SiteEntry[] sites = config.getSites();
         for (int i=0; i<sites.length; i++) {
             FeatureEntry f = sites[i].getFeatureEntry(id);
             if (f != null)
                 return f;
         }
         return null;
     }

     /*
      * @see IPlatformConfiguration#getConfigurationLocation()
      */
     public URL getConfigurationLocation() {
         return configLocation;
     }

     /*
      * @see IPlatformConfiguration#getChangeStamp()
      */
     public long getChangeStamp() {
         if (config.getLinkedConfig() == null)
             return config.getDate().getTime();
         return Math.max(config.getDate().getTime(), config.getLinkedConfig().getDate().getTime());
     }

     /*
      * @see IPlatformConfiguration#getFeaturesChangeStamp()
      * @deprecated Don't use this method
      */
     public long getFeaturesChangeStamp() {
         return 0;
     }

     /*
      * @see IPlatformConfiguration#getPluginsChangeStamp()
      * @deprecated Don't use this method
      */
     public long getPluginsChangeStamp() {
         return 0;
     }


     public String getApplicationIdentifier() {
         // Return the app if defined in system properties
 String application = ConfigurationActivator.getBundleContext().getProperty(ECLIPSE_APPLICATION);
         if (application != null)
             return application;

         // Otherwise, try to get it from the primary feature (aka product)
 String feature = getPrimaryFeatureIdentifier();

         // lookup application for feature (specified or defaulted)
 if (feature != null) {
             IFeatureEntry fe = findConfiguredFeatureEntry(feature);
             if (fe != null) {
                 if (fe.getFeatureApplication() != null)
                     return fe.getFeatureApplication();
             }
         }

         // return hardcoded default if we failed
 return DEFAULT_FEATURE_APPLICATION;
     }

     /*
      * @see IPlatformConfiguration#getPrimaryFeatureIdentifier()
      */
     public String getPrimaryFeatureIdentifier() {
         // Return the product if defined in system properties
 String primaryFeatureId = ConfigurationActivator.getBundleContext().getProperty(ECLIPSE_PRODUCT);
         if (primaryFeatureId != null) {
             // check if feature exists
 IFeatureEntry feature = findConfiguredFeatureEntry(primaryFeatureId);
             if (feature != null && feature.canBePrimary())
                 return primaryFeatureId;
         }
         return null;
     }

     /*
      * @see IPlatformConfiguration#getPluginPath()
      */
     public URL [] getPluginPath() {
         ArrayList path = new ArrayList ();
         Utils.debug("computed plug-in path:"); //$NON-NLS-1$

         ISiteEntry[] sites = getConfiguredSites();
         URL pathURL;
         for (int i = 0; i < sites.length; i++) {
             String [] plugins = sites[i].getPlugins();
             for (int j = 0; j < plugins.length; j++) {
                 try {
                     pathURL = new URL (((SiteEntry) sites[i]).getResolvedURL(), plugins[j]);
                     path.add(pathURL);
                     Utils.debug(" " + pathURL.toString()); //$NON-NLS-1$
 } catch (MalformedURLException e) {
                     // skip entry ...
 Utils.debug(" bad URL: " + e); //$NON-NLS-1$
 }
             }
         }
         return (URL []) path.toArray(new URL [0]);
     }
     
     public Set getPluginPaths() {
         
         HashSet paths = new HashSet ();
         ISiteEntry[] sites = getConfiguredSites();

         for (int i = 0; i < sites.length; i++) {
             String [] plugins = sites[i].getPlugins();
             for (int j = 0; j < plugins.length; j++) {
                     paths.add(plugins[j]);
             }
         }
         
         return paths;
     }
     

     /*
      * A variation of the getPluginPath, but it returns the actual plugin entries
      */
     public PluginEntry[] getPlugins() {
         ArrayList allPlugins = new ArrayList ();
         Utils.debug("computed plug-ins:"); //$NON-NLS-1$

         ISiteEntry[] sites = getConfiguredSites();
         for (int i = 0; i < sites.length; i++) {
             if (!(sites[i] instanceof SiteEntry)) {
                 Utils.debug("Site " + sites[i].getURL() + " is not a SiteEntry"); //$NON-NLS-1$ //$NON-NLS-2$
 continue;
             }
             PluginEntry[] plugins = ((SiteEntry)sites[i]).getPluginEntries();
             for (int j = 0; j < plugins.length; j++) {
                 allPlugins.add(plugins[j]);
                 Utils.debug(" " + plugins[j].getURL()); //$NON-NLS-1$
 }
         }
         return (PluginEntry[]) allPlugins.toArray(new PluginEntry[0]);
     }
     

     /*
      * @see IPlatformConfiguration#getBootstrapPluginIdentifiers()
      */
     public String [] getBootstrapPluginIdentifiers() {
         return BOOTSTRAP_PLUGINS;
     }

     /*
      * @see IPlatformConfiguration#setBootstrapPluginLocation(String, URL)
      */
     public void setBootstrapPluginLocation(String id, URL location) {
     }

     /*
      * @see IPlatformConfiguration#isUpdateable()
      */
     public boolean isUpdateable() {
         return true;
     }

     /*
      * @see IPlatformConfiguration#isTransient()
      */
     public boolean isTransient() {
         return (config != null) ? config.isTransient() : false;
     }

     /*
      * @see IPlatformConfiguration#isTransient(boolean)
      */
     public void isTransient(boolean value) {
         if (this != getCurrent() && config != null)
             config.setTransient(value);
     }

     /*
      * @see IPlatformConfiguration#refresh()
      */
     public synchronized void refresh() {
         // Reset computed values. Will be lazily refreshed
 // on next access
 ISiteEntry[] sites = getConfiguredSites();
         for (int i = 0; i < sites.length; i++) {
             if (sites[i].isUpdateable()) {
                 // reset site entry
 ((SiteEntry) sites[i]).refresh();
             }
         }
     }

     /*
      * @see IPlatformConfiguration#save()
      */
     public void save() throws IOException {
         if (isUpdateable())
             save(configLocation);
     }

     /*
      * @see IPlatformConfiguration#save(URL)
      */
     public synchronized void save(URL url) throws IOException {
         if (url == null)
             throw new IOException (Messages.cfig_unableToSave_noURL);

         OutputStream os = null;
         if (!url.getProtocol().equals("file")) { //$NON-NLS-1$
 // not a file protocol - attempt to save to the URL
 URLConnection uc = url.openConnection();
             uc.setDoOutput(true);
             os = uc.getOutputStream();
             try {
                 saveAsXML(os);
                 config.setDirty(false);
             } catch (CoreException e) {
                 Utils.log(e.getMessage());
                 Utils.log(e.getStatus());
                 throw new IOException (NLS.bind(Messages.cfig_unableToSave, (new String [] { url.toExternalForm() })));
             } finally {
                 os.close();
             }
         } else {
             // file protocol - do safe i/o
 File cfigFile = new File (url.getFile().replace('/', File.separatorChar));
             if (!cfigFile.getName().equals(PLATFORM_XML)) {
                 if (cfigFile.exists() && cfigFile.isFile()) {
                     Utils.log(Messages.PlatformConfiguration_expectingPlatformXMLorDirectory + cfigFile.getName());
                     cfigFile = cfigFile.getParentFile();
                 }
                 cfigFile = new File (cfigFile, CONFIG_NAME);
             }
             File workingDir = cfigFile.getParentFile();
             if (workingDir != null && !workingDir.exists())
                 workingDir.mkdirs();

             // Do safe i/o:
 // - backup current config, by moving it to the history folder
 // - write new config to platform.xml.tmp file
 // - rename the temp file to platform.xml
 File cfigFileOriginal = new File (cfigFile.getAbsolutePath());
             File cfigTmp = new File (cfigFile.getAbsolutePath() + CONFIG_FILE_TEMP_SUFFIX);
             
             // Backup old file
 if (cfigFile.exists()){
                 File backupDir = new File (workingDir, CONFIG_HISTORY);
                 if (!backupDir.exists())
                     backupDir.mkdir();
                 long timestamp = cfigFile.lastModified();
                 File preservedFile = new File (backupDir, String.valueOf(timestamp)+".xml"); //$NON-NLS-1$
 // If the target file exists, increment the timestamp. Try at most 100 times.
 long increment = 1;
                 while (preservedFile.exists() && increment < 100){
                     preservedFile = new File (backupDir, String.valueOf(timestamp+increment++)+".xml"); //$NON-NLS-1$
 }
                 if (!preservedFile.exists()) {
                     // try renaming current config to backup copy
 if (!cfigFile.renameTo(preservedFile))
                         Utils.log(Messages.PlatformConfiguration_cannotBackupConfig);
                 }
             }

             // first save the file as temp
 os = new FileOutputStream (cfigTmp);
             
             try {
                 saveAsXML(os);
                 // Try flushing any internal buffers, and synchronize with the disk
 try {
                     os.flush();
                     ((FileOutputStream )os).getFD().sync();
                 } catch (SyncFailedException e2) {
                     Utils.log(e2.getMessage());
                 } catch (IOException e2) {
                     Utils.log(e2.getMessage());
                 }
                 try {
                     os.close();
                     os = null;
                 } catch (IOException e1) {
                     Utils.log(Messages.PlatformConfiguration_cannotCloseStream + cfigTmp);
                     Utils.log(e1.getMessage());
                 }
                 // set file time stamp to match that of the config element
 cfigTmp.setLastModified(config.getDate().getTime());
                 // set this on config, in case the value was rounded off
 config.setLastModified(cfigTmp.lastModified());
                 // make the change stamp to be the same as the config file
 changeStamp = config.getDate().getTime();
                 config.setDirty(false);
             } catch (CoreException e) {
                 throw new IOException (NLS.bind(Messages.cfig_unableToSave, (new String [] { cfigTmp.getAbsolutePath() })));
             } finally {
                 if (os != null)
                     try {
                         os.close();
                     } catch (IOException e1) {
                         Utils.log(Messages.PlatformConfiguration_cannotCloseTempFile + cfigTmp);
                     }
             }

             // at this point we have old config (if existed) as "bak" and the
 // new config as "tmp".
 boolean ok = cfigTmp.renameTo(cfigFileOriginal);
             if (!ok) {
                 // this codepath represents a tiny failure window. The load processing
 // on startup will detect missing config and will attempt to start
 // with "tmp" (latest), then "bak" (the previous). We can also end up
 // here if we failed to rename the current config to "bak". In that
 // case we will restart with the previous state.
 Utils.log(Messages.PlatformConfiguration_cannotRenameTempFile);
                 
                 throw new IOException (NLS.bind(Messages.cfig_unableToSave, (new String [] { cfigTmp.getAbsolutePath() })));
             }
         }
     }


     public static PlatformConfiguration getCurrent() {
         return currentPlatformConfiguration;
     }

     /**
      * Starts a platform installed at specified installURL using configuration located at platformConfigLocation.
      */
     public static synchronized void startup(URL installURL, Location platformConfigLocation) throws Exception {
         PlatformConfiguration.installURL = installURL;
     
         // create current configuration
 if (currentPlatformConfiguration == null) {
             currentPlatformConfiguration = new PlatformConfiguration(platformConfigLocation);
             if (currentPlatformConfiguration.config == null)
                 throw new Exception (Messages.PlatformConfiguration_cannotLoadConfig + platformConfigLocation.getURL());
             if (currentPlatformConfiguration.config.isDirty())
                 // If this is a transient config (generated by PDE),do nothing
 // otherwise, save the configuration with proper date
 if (!currentPlatformConfiguration.isTransient())
                     currentPlatformConfiguration.save();
         }
     }

     public static synchronized void shutdown() throws IOException {

         // save platform configuration
 PlatformConfiguration config = getCurrent();
         if (config != null) {
             // only save if there are changes in the config
 if (config.config.isDirty() && !config.isTransient()) {
                 try {
                     config.save();
                 } catch (IOException e) {
                     Utils.debug("Unable to save configuration " + e.toString()); //$NON-NLS-1$
 // will recover on next startup
 }
             }
         }
     }


     private synchronized void initializeCurrent(Location platformConfigLocation) throws IOException {

         // Configuration URL was is specified by the OSGi layer.
 // Default behavior is to look
 // for configuration in the specified meta area. If not found, look
 // for pre-initialized configuration in the installation location.
 // If it is found it is used as the initial configuration. Otherwise
 // a new configuration is created. In either case the resulting
 // configuration is written into the specified configuration area.

         URL configFileURL = new URL (platformConfigLocation.getURL(), CONFIG_NAME);
         try {
             // check concurrent use lock
 getConfigurationLock(platformConfigLocation.getURL());

             // try loading the configuration
 try {
                 config = loadConfig(configFileURL);
                 Utils.debug("Using configuration " + configFileURL.toString()); //$NON-NLS-1$
 } catch (Exception e) {
                 // failed to load, see if we can find pre-initialized configuration.
 try {
                     Location parentLocation = platformConfigLocation.getParentLocation();
                     if (parentLocation == null)
                         throw new IOException (); // no platform.xml found, need to create default site

                     URL sharedConfigFileURL = new URL (parentLocation.getURL(), CONFIG_NAME);
                     config = loadConfig(sharedConfigFileURL);
                     
                     // pre-initialized config loaded OK ... copy any remaining update metadata
 // Only copy if the default config location is not the install location
 if (!sharedConfigFileURL.equals(configFileURL)) {
                         // need to link config info instead of using a copy
 linkInitializedState(config, parentLocation, platformConfigLocation);
                         Utils.debug("Configuration initialized from " + sharedConfigFileURL.toString()); //$NON-NLS-1$
 }
                     return;
                 } catch (Exception ioe) {
                     Utils.debug("Creating default configuration from " + configFileURL.toExternalForm()); //$NON-NLS-1$
 createDefaultConfiguration(configFileURL);
                 }
             } finally {
                 // if config == null an unhandled exception has been thrown and we allow it to propagate
 if (config != null) {
                     configLocation = configFileURL;
                      if (config.getURL() == null)
                         config.setURL(configFileURL);
                     verifyPath(configLocation);
                     Utils.debug("Creating configuration " + configFileURL.toString()); //$NON-NLS-1$
 }
             }
         } finally {
             // releaes concurrent use lock
 clearConfigurationLock();
         }
     }

     
     private synchronized void initialize(URL url) throws Exception {
         if (url != null) {
             config = loadConfig(url);
             Utils.debug("Using configuration " + url.toString()); //$NON-NLS-1$
 }
         if (config == null) {
             config = new Configuration();
             Utils.debug("Creating empty configuration object"); //$NON-NLS-1$
 }
         config.setURL(url);
         configLocation = url;
     }

     private void createDefaultConfiguration(URL url)throws IOException {
         // we are creating new configuration
 config = new Configuration();
         config.setURL(url);
         SiteEntry defaultSite = (SiteEntry)getRootSite();
         configureSite(defaultSite);
         try {
             // parse the site directory to discover features
 defaultSite.loadFromDisk(0);
         } catch (CoreException e1) {
             Utils.log(Messages.PlatformConfiguration_cannotLoadDefaultSite + defaultSite.getResolvedURL());
             return;
         }
     }
     private ISiteEntry getRootSite() {
         // create default site entry for the root
 ISitePolicy defaultPolicy = createSitePolicy(getDefaultPolicy(), DEFAULT_POLICY_LIST);
         URL siteURL = null;
         try {
             siteURL = new URL ("platform:/base/"); //$NON-NLS-1$ // try using platform-relative URL
 } catch (MalformedURLException e) {
             siteURL = getInstallURL(); // ensure we come up ... use absolute file URL
 }
         ISiteEntry defaultSite = createSiteEntry(siteURL, defaultPolicy);
         return defaultSite;
     }
     
     /**
      * Gets the configuration lock
      * @param url configuration directory
      */
     private void getConfigurationLock(URL url) {
         if (!url.getProtocol().equals("file")) //$NON-NLS-1$
 return;

         File lockFile = new File (url.getFile(), ConfigurationActivator.NAME_SPACE+ File.separator+CONFIG_FILE_LOCK_SUFFIX);
         verifyPath(url);
         // PAL nio optional
 lock = createLocker(lockFile);
         try {
             lock.lock();
         } catch (IOException ioe) {
             lock = null;
         }
     }
     
     private void clearConfigurationLock() {
         // PAL nio optional
 if (lock != null) {
             lock.release();
         }
     }
     
     /**
      * Create a locker using java new I/O or regular I/O
      * depending whether we run in J2SE or cdcFoundation
      * PAL nio optional
      */
     private static Locker createLocker(File lock) {
         if (!checkNio) {
             useNio = true;
             try {
                  Class.forName("java.nio.channels.FileLock"); //$NON-NLS-1$
 } catch (ClassNotFoundException e) {
                 useNio = false;
             }
         }
         if (useNio)
             return new Locker_JavaNio(lock);
         
         return new Locker_JavaIo(lock);
     }
     
     private long computeChangeStamp() {
         featuresChangeStamp = computeFeaturesChangeStamp();
         pluginsChangeStamp = computePluginsChangeStamp();
         changeStamp = Math.max(featuresChangeStamp, pluginsChangeStamp);
         // round off to seconds
 changeStamp = (changeStamp/1000)*1000;
         return changeStamp;
     }

     private long computeFeaturesChangeStamp() {
         if (featuresChangeStampIsValid)
             return featuresChangeStamp;

         long result = 0;
         ISiteEntry[] sites = config.getSites();
         for (int i = 0; i < sites.length; i++) {
             result = Math.max(result, sites[i].getFeaturesChangeStamp());
         }
         featuresChangeStamp = result;
         featuresChangeStampIsValid = true;
         return featuresChangeStamp;
     }

     private long computePluginsChangeStamp() {
         if (pluginsChangeStampIsValid)
             return pluginsChangeStamp;

         long result = 0;
         ISiteEntry[] sites = config.getSites();
         for (int i = 0; i < sites.length; i++) {
             result = Math.max(result, sites[i].getPluginsChangeStamp());
         }
         pluginsChangeStamp = result;
         pluginsChangeStampIsValid = true;
         return pluginsChangeStamp;
     }

     private void configureExternalLinks() {
         URL linkURL = getInstallURL();
         if (!supportsDetection(linkURL))
             return;

         try {
             linkURL = new URL (linkURL, LINKS + "/"); //$NON-NLS-1$
 } catch (MalformedURLException e) {
             // skip bad links ...
 Utils.debug("Unable to obtain link URL"); //$NON-NLS-1$
 return;
         }

         File linkDir = new File (linkURL.getFile());
         File [] links = linkDir.listFiles();
         if (links == null || links.length == 0) {
             Utils.debug("No links detected in " + linkURL.toExternalForm()); //$NON-NLS-1$
 return;
         }

         for (int i = 0; i < links.length; i++) {
             if (links[i].isDirectory())
                 continue;
             Utils.debug("Link file " + links[i].getAbsolutePath()); //$NON-NLS-1$
 Properties props = new Properties ();
             FileInputStream is = null;
             try {
                 is = new FileInputStream (links[i]);
                 props.load(is);
                 configureExternalLinkSite(links[i], props);
             } catch (IOException e) {
                 // skip bad links ...
 Utils.debug(" unable to load link file " + e); //$NON-NLS-1$
 continue;
             } finally {
                 if (is != null) {
                     try {
                         is.close();
                     } catch (IOException e) {
                         // ignore ...
 }
                 }
             }
         }
     }

     private void configureExternalLinkSite(File linkFile, Properties props) {
         String path = props.getProperty(LINK_PATH);
         if (path == null) {
             Utils.debug(" no path definition"); //$NON-NLS-1$
 return;
         }

         String link;
         boolean updateable = true;
         URL siteURL;

         // parse out link information
 if (path.startsWith(LINK_READ + " ")) { //$NON-NLS-1$
 updateable = false;
             link = path.substring(2).trim();
         } else if (path.startsWith(LINK_READ_WRITE + " ")) { //$NON-NLS-1$
 link = path.substring(3).trim();
         } else {
             link = path.trim();
         }

         // make sure we have a valid link specification
 try {
             File siteFile = new File (link);
             siteFile = new File (siteFile, ECLIPSE);
             siteURL = siteFile.toURL();
             if (findConfiguredSite(siteURL, true) != null)
                 // linked site is already known
 return;
         } catch (MalformedURLException e) {
             // ignore bad links ...
 Utils.debug(" bad URL " + e); //$NON-NLS-1$
 return;
         }
         
         // process the link
 SiteEntry linkSite = (SiteEntry) externalLinkSites.get(siteURL);
         if (linkSite == null) {
             // this is a link to a new target so create site for it
 ISitePolicy linkSitePolicy = createSitePolicy(getDefaultPolicy(), DEFAULT_POLICY_LIST);
             linkSite = (SiteEntry) createSiteEntry(siteURL, linkSitePolicy);
         }
         // update site entry if needed
 linkSite.setUpdateable(updateable);
         linkSite.setLinkFileName(linkFile.getAbsolutePath());

         // configure the new site
 // NOTE: duplicates are not replaced (first one in wins)
 configureSite(linkSite);
         // there are changes in the config
 config.setDirty(true);
         Utils.debug(" " + (updateable ? "R/W -> " : "R/O -> ") + siteURL.toString()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
 }

    private void validateSites() {

        // check to see if all sites are valid. Remove any sites that do not exist.
 SiteEntry[] list = config.getSites();
        for (int i = 0; i < list.length; i++) {
            URL siteURL = list[i].getResolvedURL();
            if (!supportsDetection(siteURL))
                continue;

            File siteRoot = new File (siteURL.getFile().replace('/', File.separatorChar));
            if (!siteRoot.exists()) {
                unconfigureSite(list[i]);
                Utils.debug("Site " + siteURL + " does not exist ... removing from configuration"); //$NON-NLS-1$ //$NON-NLS-2$
 }
            
            // If multiple paths are defined in the same link file
 // or if the path changes, the old site will still be kept.
 // A better algorithm could be implemented by keeping track
 // of the previous content of the link file.
 // TODO do the above
 String linkName = list[i].getLinkFileName();
            if (linkName != null) {
                File linkFile = new File (linkName);
                if (!linkFile.exists()) {
                    unconfigureSite(list[i]);
                    config.setDirty(true);
                    Utils.debug("Site " + siteURL + " is no longer linked ... removing from configuration"); //$NON-NLS-1$ //$NON-NLS-2$
 }
            }
        }
    }
    
    private void linkInitializedState(Configuration sharedConfig, Location sharedConfigLocation, Location newConfigLocation) {
        try {
            URL newConfigIniURL = new URL (newConfigLocation.getURL(), CONFIG_INI);
            if (!newConfigIniURL.getProtocol().equals("file")) //$NON-NLS-1$
 return; // need to be able to do write

            // modify config.ini and platform.xml to only link original files
 File configIni = new File (newConfigIniURL.getFile());
            Properties props = new Properties ();
            String externalForm = Utils.makeRelative(Utils.getInstallURL(), sharedConfigLocation.getURL()).toExternalForm();
            props.put("osgi.sharedConfiguration.area", externalForm); //$NON-NLS-1$
 props.store(new FileOutputStream (configIni), "Linked configuration"); //$NON-NLS-1$

            config = new Configuration(new Date ());
            config.setURL(new URL (newConfigLocation.getURL(), CONFIG_NAME));
            config.setLinkedConfig(sharedConfig);
            config.setDirty(true);
        } catch (IOException e) {
            // this is an optimistic copy. If we fail, the state will be reconciled
 // when the update manager is triggered.
 System.out.println(e);
        }
    }
    
    private Configuration loadConfig(URL url) throws Exception {
        if (url == null)
            throw new IOException (Messages.cfig_unableToLoad_noURL);

        // try to load saved configuration file (watch for failed prior save())
 ConfigurationParser parser = null;
        try {
            parser = new ConfigurationParser();
        } catch (InvocationTargetException e) {
            throw (Exception )e.getTargetException();
        }
        
        config = null;
        Exception originalException = null;
        try {
            config = parser.parse(url);
            if (config == null)
                throw new Exception (Messages.PlatformConfiguration_cannotFindConfigFile);
        } catch (Exception e1) {
            // check for save failures, so open temp and backup configurations
 originalException = e1;
            try {
                URL tempURL = new URL (url.toExternalForm()+CONFIG_FILE_TEMP_SUFFIX);
                config = parser.parse(tempURL);
                if (config == null)
                    throw new Exception ();
                config.setDirty(true); // force saving to platform.xml
 } catch (Exception e2) {
                try {
                    // check the backup
 if ("file".equals(url.getProtocol())) { //$NON-NLS-1$
 File cfigFile = new File (url.getFile().replace('/', File.separatorChar));
                        File workingDir = cfigFile.getParentFile();
                        if (workingDir != null && workingDir.exists()) {
                            File [] backups = workingDir.listFiles(new FileFilter (){
                                public boolean accept(File pathname) {
                                    return pathname.isFile() && pathname.getName().endsWith(".xml"); //$NON-NLS-1$
 }});
                            if (backups != null && backups.length > 0) {
                                URL backupUrl = backups[backups.length-1].toURL();
                                config = parser.parse(backupUrl);
                            }
                        }
                    }
                    if (config == null)
                        throw originalException; // we tried, but no config here ...
 config.setDirty(true); // force saving to platform.xml
 } catch (IOException e3) {
                    throw originalException; // we tried, but no config here ...
 }
            }
        }

        return config;
    }

    public static boolean supportsDetection(URL url) {
        String protocol = url.getProtocol();
        if (protocol.equals("file")) //$NON-NLS-1$
 return true;
        else if (protocol.equals("platform")) { //$NON-NLS-1$
 URL resolved = null;
            try {
                resolved = resolvePlatformURL(url); // 19536
 } catch (IOException e) {
                return false; // we tried but failed to resolve the platform URL
 }
            return resolved.getProtocol().equals("file"); //$NON-NLS-1$
 } else
            return false;
    }

    private static void verifyPath(URL url) {
        String protocol = url.getProtocol();
        String path = null;
        if (protocol.equals("file")) //$NON-NLS-1$
 path = url.getFile();
        else if (protocol.equals("platform")) { //$NON-NLS-1$
 URL resolved = null;
            try {
                resolved = resolvePlatformURL(url); // 19536
 if (resolved.getProtocol().equals("file")) //$NON-NLS-1$
 path = resolved.getFile();
            } catch (IOException e) {
                // continue ...
 }
        }

        if (path != null) {
            File dir = new File (path).getParentFile();
            if (dir != null)
                dir.mkdirs();
        }
    }

    public static URL resolvePlatformURL(URL url) throws IOException {
        if (url.getProtocol().equals("platform")) { //$NON-NLS-1$
 url = FileLocator.toFileURL(url);
            File f = new File (url.getFile());
            url = f.toURL();
        }
        return url;
    }
    
    public static URL getInstallURL() {
        return installURL;
    }
    
    // Patch from Phil to support cdc/foundation: will use the method below instead of this one.
// private void saveAsXML(OutputStream stream) throws CoreException {
// StreamResult result = null;
// try {
// DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder();
// Document doc = docBuilder.newDocument();
//
// if (config == null)
// throw Utils.newCoreException(Messages.getString("PlatformConfiguration.cannotSaveNonExistingConfig"),null); //$NON-NLS-1$
//
// config.setDate(new Date());
// doc.appendChild(doc.createComment("Created on " + config.getDate().toString())); //$NON-NLS-1$
// Element configElement = config.toXML(doc);
// doc.appendChild(configElement);
//
// // Write out to a file
//
// Transformer transformer=transformerFactory.newTransformer();
// transformer.setOutputProperty(OutputKeys.METHOD, "xml"); //$NON-NLS-1$
// transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); //$NON-NLS-1$
// transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$
// DOMSource source = new DOMSource(doc);
// result = new StreamResult(stream);
//
// transformer.transform(source,result);
// //will close the stream in the caller
// //stream.close();
// } catch (Exception e) {
// throw Utils.newCoreException("", e); //$NON-NLS-1$
// } finally {
// result.setOutputStream(null);
// result = null;
// }
// }

    private void saveAsXML(OutputStream stream) throws CoreException,IOException {
        BufferedWriter xmlWriter = new BufferedWriter (new OutputStreamWriter (stream,XML_ENCODING));
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setExpandEntityReferences(false);
            factory.setValidating(false);
            factory.setIgnoringComments(true);
            DocumentBuilder docBuilder = factory.newDocumentBuilder();
            Document doc = docBuilder.newDocument();
            
            if (config == null)
                throw Utils.newCoreException(Messages.PlatformConfiguration_cannotSaveNonExistingConfig,null);
            
            config.setDate(new Date ());
            Element configElement = config.toXML(doc);
            doc.appendChild(configElement);
            
            // This is not DBCS friendly... PAL
 //XMLPrintHandler.printComment(xmlWriter,"Created on " + config.getDate().toString());
 XMLPrintHandler.printNode(xmlWriter,doc,XML_ENCODING);
            
        } catch (Exception e) {
            throw Utils.newCoreException("", e); //$NON-NLS-1$
 } finally {
            xmlWriter.flush();
            // will close the stream in the caller
 //xmlWriter.close();
 }
    }
    
    private void reconcile() throws CoreException {
        long lastChange = config.getDate().getTime();
        SiteEntry[] sites = config.getSites();
        for (int s = 0; s < sites.length; s++) {
            if (sites[s].isUpdateable()) {
                long siteTimestamp = sites[s].getChangeStamp();
                if (siteTimestamp > lastChange)
                    sites[s].loadFromDisk(lastChange);
            }
        }
        config.setDirty(true);
    }
    
    public Configuration getConfiguration() {
        return config;
    }
}

